package serverComponent;

import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;

import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
import org.jivesoftware.whack.util.StringUtils;
import org.xmpp.component.AbstractComponent;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.PacketExtension;

public class IQHandler
{
	private PubSubService pubSubService = null;
	
	// This class contains information about a pending subscription for a user
	private class PendingSubscriptionDetails
	{
		private JID fromJID;
		private String subscribingResource;
		private String subscribingNode;
		
		public PendingSubscriptionDetails( JID fromJID, String subscribingResource, String node )
		{
			setFromJID( fromJID );
			setSubscribingNode( node );
			setSubscribingResource( subscribingResource );
		}

		public JID getFromJID() {
			return fromJID;
		}
		private void setFromJID(JID fullJID) {
			this.fromJID = fullJID;
		}
		
		public String getSubscribingNode() {
			return subscribingNode;
		}
		private void setSubscribingNode(String subscriptionNode) {
			this.subscribingNode = subscriptionNode;
		}

		public void setSubscribingResource( String subscribingResource ) {
			this.subscribingResource = subscribingResource;
		}
		public String getSubscribingResource() {
			return subscribingResource;
		}
	};
	
	// This class contains information about a disco-items IQ made from the server
	private class PendingDiscoInfoDetails
	{
		// itemList is the list of items for which a disco#info has to be done
		private Stack<String> itemList;
		// requestID is the ID of the corresponding disco#item request 
		private String requestID;
		
		public PendingDiscoInfoDetails( String requestID )
		{
			itemList = new Stack<String>();
			this.requestID = requestID ;
		}
		
		public String getRequestID() {
			return requestID;
		}
		@SuppressWarnings( "unused" )
		public void setRequestID( String requestID ) {
			this.requestID = requestID;
		}
		
		public String getNextItem()
		{
			if( itemList.isEmpty())
				return null;
			else
				return itemList.pop();
		}
		public void addItem( String item ) {
			itemList.push( item );
		}
		public boolean isEmpty() {
			return itemList.isEmpty();
		}	
	}
		
	// It contains the mapping between service provider domain and service provider pubsub subdomain
	private Map<String, String> pubSubDomain;
	/*
	 * It contains the mapping between request id of user subscribe request and subscribe info such as
	 * user full jid and  server provider subscription node
	*/
	private Map<String, PendingSubscriptionDetails> pendingInSubscribe;
	/*
	 * It contains the mapping between request id of cloud forwarded subscribe request 
	 * and user subscribe request
	*/
	private Map<String, String> pendingOutSubscribe;
	/*
	 * It contains the mapping between request id of user unsubscribe request and subscribe info such as
	 * user full jid and  server provider subscription node
	*/
	private Map<String, PendingSubscriptionDetails> pendingInUnsubscribe;
	/*
	 * It contains the mapping between request id of cloud forwarded unsubscribe request 
	 * and user unsubscribe request
	*/
	private Map<String, String> pendingOutUnsubscribe;
	/*
	 * It contains the mapping between request id of cloud disco#item info 
	 * and user request
	*/
	private Map<String, String> pendingDiscoItem;
	/*
	 * It contains the mapping between request id of disco#info request
	 * and detailed data about this request
	*/
	private Map<String, PendingDiscoInfoDetails> pendingDiscoInfo;
		
	public IQHandler( PubSubService pubSubService ) {
		this.pubSubService = pubSubService;
	}
	
	public void processIQ( IQ request )
	{
		IQ.Type requestType = request.getType();
		
		if( requestType == IQ.Type.set )
		{
			Element childElement = request.getChildElement();
			String namespace = childElement.getNamespaceURI();
						
			if( PubSubNamespace.BASIC.getXmlns().compareTo( namespace ) == 0 &&
					childElement.getName().compareTo("pubsub") == 0 )
			{
				if( childElement.element("subscribe") != null )
					processSubscribeRequest( request, childElement );
				else if( childElement.element("unsubscribe") != null )
					processUnsubscribeRequest( request, childElement );
			}
		}
		else if( requestType == IQ.Type.result )
		{
			Element childElement = request.getChildElement();
			
			if( childElement == null )
				processEmptyResult( request );
			else
			{
				String namespace = childElement.getNamespaceURI();
				if( PubSubNamespace.BASIC.getXmlns().compareTo( namespace ) == 0 &&
						childElement.getName().compareTo("pubsub") == 0 &&
						childElement.element("subscription") != null )
					processSubscriptionResult( request, childElement );
				else if( namespace.compareTo( AbstractComponent.NAMESPACE_DISCO_ITEMS ) == 0 &&
						childElement.getName().compareTo("query") == 0 )
					processResultDiscoItems( request, childElement );
				else if( namespace.compareTo( AbstractComponent.NAMESPACE_DISCO_INFO ) == 0 &&
						childElement.getName().compareTo("query") == 0 )
					processResultDiscoInfo( request, childElement );
			}
		}
		else if( requestType == IQ.Type.error )
		{
			if( pendingOutSubscribe.containsKey( request.getID()))
				errorOutSubscriptionRequest( request );
			else if( pendingDiscoItem.containsKey( request.getID()))
				errorOutDiscoItemRequest( request );
			else if( pendingDiscoInfo.containsKey( request.getID()))
				processNextDiscoItem( request );
		}		
	}
	
	private void processSubscribeRequest( IQ request, Element childElement )
	{
		Element subscribeElement = childElement.element("subscribe");
		String subscribeNode = subscribeElement.attributeValue("node");
		JID subscriberJID = new JID( subscribeElement.attributeValue("jid"));
		
		// Check if node attribute is present within subscribe element and is not null
		if( subscribeNode == null || subscribeNode.length() == 0 )
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.bad_request ), 
					DocumentHelper.createElement( QName.get("nodeid-required", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		// Check if JID attribute is present within subscribe element and is not null
		if( subscriberJID == null || subscriberJID.toString().length() == 0 )
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.bad_request ), 
					DocumentHelper.createElement( QName.get("jid-required", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		// Check if subscriber bare JID and from field bare JID are the same
		if( subscriberJID.toBareJID().compareTo( request.getFrom().toBareJID()) != 0 )
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.bad_request ), 
					DocumentHelper.createElement( QName.get("invalid-jid", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		
		String subscriberBareJID = subscriberJID.toBareJID();
		// Check if already exists any subscription for the same bare JID
		if( pubSubService.hasSubscriptions( subscriberBareJID ))
		{
			Map<String, SubscriptionDetails> userSubscriptions = pubSubService.getSubscriptions( subscriberBareJID );
			// Check if the user already has an active subscription for the same node
			if( userSubscriptions.containsKey( subscribeNode ))
			{
				SubscriptionDetails subscriptionDetails = userSubscriptions.get( subscribeNode );
				
				// Register the subscription of the resource specified within JID attribute
				if( subscriberJID.getResource() == null )
					subscriptionDetails.setSubscribedWithBareJID(true);
				else
					subscriptionDetails.addResource( subscriberJID.getResource());
				
				// Send to the user a result IQ packet in order to signal the registration
				IQ response = new IQ( IQ.Type.result );
				
				response.setID( request.getID());
				response.setFrom( pubSubService.getName());
				response.setTo( request.getFrom());
				response.setChildElement("pubsub", PubSubNamespace.BASIC.getXmlns());
				
				Element subscriptionElement = DocumentHelper.createElement("subscription");
				subscriptionElement.addAttribute("node", subscribeNode );
				subscriptionElement.addAttribute("jid", subscriberJID.toString());
				subscriptionElement.addAttribute("subscription", "subscribed");
				
				if( subscriptionDetails.getSubscriptionID() != null )
					subscriptionElement.addAttribute("subid", subscriptionDetails.getSubscriptionID());
				
				PacketExtension subscriptionExtension = new PacketExtension( subscriptionElement );
				response.addExtension( subscriptionExtension );
				
				pubSubService.sendPacket( response );
				
				return ;
			}
		}

		// TODO: Check parseServer
		String serviceDomain = StringUtils.parseServer( subscribeNode );
		// Check if subscribeNode has a domain specified
		if( serviceDomain.length() == 0 )
		{
			// Signal that the node cannot be found because the domain part is missing in the node attribute
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.item_not_found, PacketError.Type.cancel ), 
					null );
			return ;
		}
		
		/* 
		 * If user has no active subscriptions or he has no subscriptions for the specified node:
		 * 	-	Save subscription request details in order to get back the data when a reply from the server
		 * 		responsible for the specified node is received
		 * 	-	Check if already exists a mapping between service domain and pubsub service 
		 */
		pendingInSubscribe.put( request.getID(), 
				new PendingSubscriptionDetails( request.getFrom(), subscriberJID.getResource(), subscribeNode ));
		
		// Check if already exists a mapping between service domain and pubsub service
		if( pubSubDomain.containsKey( serviceDomain ))
		{
			// Send a subscribe request to the specific server service
			IQ forwardedRequest = new IQ( IQ.Type.set );
			
			forwardedRequest.setFrom( pubSubService.getName());
			forwardedRequest.setTo( pubSubDomain.get( serviceDomain ));
			forwardedRequest.setChildElement( childElement.createCopy());
			
			pendingOutSubscribe.put( forwardedRequest.getID(), request.getID());
			pubSubService.sendPacket( forwardedRequest );
		}
		else
		{
			// Make an item discovery procedure in order to obtain the pubsub service address
			IQ discoItem = new IQ( IQ.Type.get );
			
			discoItem.setFrom( pubSubService.getName());
			discoItem.setTo( serviceDomain );
			discoItem.setChildElement("query", AbstractComponent.NAMESPACE_DISCO_ITEMS );
			
			pendingDiscoItem.put( discoItem.getID(), request.getID());
			pubSubService.sendPacket( discoItem );
		}			
	}
	
	private void processUnsubscribeRequest( IQ request, Element childElement )
	{
		Element unsubscribeElement = childElement.element("unsubscribe");
		String unsubscribeNode = unsubscribeElement.attributeValue("node");
		JID unsubscriberJID = new JID( unsubscribeElement.attributeValue("jid"));
		String requestSubscriptionID = unsubscribeElement.attributeValue("subid");
		
		// Check if node attribute is present within unsubscribe element and is not null
		if( unsubscribeNode == null || unsubscribeNode.length() == 0 )
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.bad_request ), 
					DocumentHelper.createElement( QName.get("nodeid-required", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		// Check if JID attribute is present within subscribe element and is not null
		if( unsubscriberJID == null || unsubscriberJID.toString().length() == 0 )
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.bad_request ), 
					DocumentHelper.createElement( QName.get("jid-required", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		
		String unsubscriberBareJID = unsubscriberJID.toBareJID();
		// Check if unsubscriber bare JID and from field bare JID are the same
		if( unsubscriberBareJID.compareTo( request.getFrom().toBareJID()) != 0 )
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.bad_request ), 
					DocumentHelper.createElement( QName.get("invalid-jid", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		
		// TODO: Check parseServer
		String serviceDomain = StringUtils.parseServer( unsubscribeNode );
		// Check if unsubscribeNode has a domain specified
		if( serviceDomain.length() == 0 )
		{
			// Signal that the node cannot be found because the domain part is missing in the node attribute
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.item_not_found, PacketError.Type.cancel ), 
					null );
			return ;
		}
		// Check if already exists any subscription for the same bare JID
		if( !pubSubService.hasSubscriptions( unsubscriberBareJID ))
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.unexpected_request, PacketError.Type.cancel ), 
					DocumentHelper.createElement( QName.get("not-subscribed", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		Map<String, SubscriptionDetails> userSubscriptions = pubSubService.getSubscriptions( unsubscriberJID.toBareJID());
			
		// Check if the user already has an active subscription for the same node
		if( !userSubscriptions.containsKey( unsubscribeNode ))
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.unexpected_request, PacketError.Type.cancel ), 
					DocumentHelper.createElement( QName.get("not-subscribed", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		SubscriptionDetails subscriptionDetails = userSubscriptions.get( unsubscribeNode );
		
		/*
		 *	If a subscription id is specified in the unsubscribe request 
		 *	check if it matches the one stored in subscriptionDetails
		 */
		if( requestSubscriptionID != null && 
				requestSubscriptionID.length() > 0 && 
				requestSubscriptionID.compareTo( subscriptionDetails.getSubscriptionID()) != 0 )
		{
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.not_acceptable ), 
					DocumentHelper.createElement( QName.get("invalid-subid", PubSubNamespace.ERROR.getXmlns())));
			return ;
		}
		
		// If the user is unsubscribing with a bare JID
		if( unsubscriberJID.getResource() == null )
		{
			// If the user isn't subscribed with a bare JID then signal the error condition
			if( !subscriptionDetails.isBareJIDSubscribed())
			{
				signalErrorCondition( request, 
						new PacketError( PacketError.Condition.unexpected_request, PacketError.Type.cancel ), 
						DocumentHelper.createElement( QName.get("not-subscribed", PubSubNamespace.ERROR.getXmlns())));
				return ;
			}
			
			/*
			 * 	If the user has other resources subscribed for the same node
			 * 	then simply signal that the unsubscribe occurs successfully 
			 */
			if( subscriptionDetails.areResourcesSubscribed())
			{
				IQ response = new IQ( IQ.Type.result );
				
				response.setID( request.getID());
				response.setFrom( pubSubService.getName());
				response.setTo( request.getFrom());
				
				// Unregister the bare JID for the specified node
				subscriptionDetails.setSubscribedWithBareJID(false);
				
				pubSubService.sendPacket( response );
			}
			/*
			 * 	If the user has no other resources subscribed for the same node
			 * 	then send an unsubscribe request to the specific server 
			 */
			else
				forwardUnsubscribeRequest( request, unsubscribeElement, subscriptionDetails.getSubscriptionID());
		}
		// If the user is unsubscribing with a full JID
		else
		{
			// If the user isn't subscribed with the specified resource then signal the error condition
			if( !subscriptionDetails.isResourceSubscribed( unsubscriberJID.getResource()))
			{
				signalErrorCondition( request, 
						new PacketError( PacketError.Condition.unexpected_request, PacketError.Type.cancel ), 
						DocumentHelper.createElement( QName.get("not-subscribed", PubSubNamespace.ERROR.getXmlns())));
				return ;
			}

			/*
			 * 	If the user has other resources subscribed for the same node
			 * 	then simply signal that the unsubscribe occurs successfully 
			 */
			if( subscriptionDetails.getResourceSize() > 1 || 
					subscriptionDetails.isBareJIDSubscribed())
			{
				IQ response = new IQ( IQ.Type.result );
				
				response.setID( request.getID());
				response.setFrom( pubSubService.getName());
				response.setTo( request.getFrom());
				
				// Unregister the specific resource for the specified node
				subscriptionDetails.removeResource( unsubscriberJID.getResource());
				
				pubSubService.sendPacket( response );
			}
			/*
			 * 	If the user has no other resources subscribed for the same node
			 * 	then send an unsubscribe request to the specific server 
			 */
			else
				forwardUnsubscribeRequest( request, unsubscribeElement, subscriptionDetails.getSubscriptionID());
		}
	}
	
	private void forwardUnsubscribeRequest( IQ request, Element unsubscribeElement, String subscriptionID )
	{
		String unsubscribeNode = unsubscribeElement.attributeValue("node");
		String unsubscriberBareJID = ( new JID( unsubscribeElement.attributeValue("jid"))).toBareJID();
		String unsubscriberResource = ( new JID( unsubscribeElement.attributeValue("jid"))).getResource();
		String serviceDomain = StringUtils.parseServer( unsubscribeNode );
		
		IQ forwardedRequest = new IQ( IQ.Type.set );
		
		forwardedRequest.setFrom( pubSubService.getName());
		forwardedRequest.setTo( pubSubDomain.get( serviceDomain ));
		forwardedRequest.setChildElement("pubsub", PubSubNamespace.BASIC.getXmlns());
		
		Element newUnsubscribeElement = DocumentHelper.createElement("unsubscribe");
		newUnsubscribeElement.addAttribute("node", unsubscribeNode );
		newUnsubscribeElement.addAttribute("jid", unsubscriberBareJID.toString());
		if( subscriptionID != null )
			newUnsubscribeElement.addAttribute("subid", subscriptionID );
		
		PacketExtension unsubscribeExtension = new PacketExtension( newUnsubscribeElement );
		forwardedRequest.addExtension( unsubscribeExtension );
		
		pendingInUnsubscribe.put( request.getID(), 
				new PendingSubscriptionDetails( request.getFrom(), unsubscriberResource, unsubscribeNode ));
		pendingOutUnsubscribe.put( forwardedRequest.getID(), request.getID());
		
		pubSubService.sendPacket( forwardedRequest );		
	}

	private void processEmptyResult( IQ request )
	{
		if( !pendingOutUnsubscribe.containsKey( request.getID()))
			return ;
	
		/*
		 *  If pendingOutUnsubscribe contains the request ID it means that the IQ  
		 *  is a result related to an unsubscribe request sent to a specific service 
		 *  server and therefore it exists a pendingInUnsubscribe item 
		 */
		String inUnsubscribeID = pendingOutUnsubscribe.get( request.getID());
		PendingSubscriptionDetails inUnsubscribe = pendingInUnsubscribe.get( inUnsubscribeID );
		
		IQ response = new IQ( IQ.Type.result );
		
		response.setID( inUnsubscribeID );
		response.setFrom( pubSubService.getName());
		response.setTo( inUnsubscribe.getFromJID());
		
		Map<String, SubscriptionDetails> userSubscriptions = 
			pubSubService.getSubscriptions( inUnsubscribe.getFromJID().toBareJID());
		
		SubscriptionDetails subscriptionDetails = 
			userSubscriptions.get( inUnsubscribe.getSubscribingNode());
		
		if( inUnsubscribe.getSubscribingResource() == null )
			subscriptionDetails.setSubscribedWithBareJID(false);
		else
			subscriptionDetails.removeResource( inUnsubscribe.getFromJID().getResource());
		
		if( !subscriptionDetails.areResourcesSubscribed() && 
				!subscriptionDetails.isBareJIDSubscribed())
			userSubscriptions.remove( inUnsubscribe.getSubscribingNode());
		
		if( userSubscriptions.size() == 0 )
			pubSubService.removeSubscription( inUnsubscribe.getFromJID().toBareJID());
		
		pendingOutUnsubscribe.remove( request.getID());
		pendingInUnsubscribe.remove( inUnsubscribeID );

		pubSubService.sendPacket( response );
	}
	
	private void processSubscriptionResult( IQ request, Element childElement )
	{
		Element subscriptionElement = childElement.element("subscription");
		String subscriptionState = subscriptionElement.attributeValue("subscription");
		
		// Check if there is an outgoing request related to the current pubsub response
		if( !pendingOutSubscribe.containsKey( request.getID()))
			return ;
		
		if( subscriptionState == null ||
				subscriptionState.length() == 0 )
		{
			// TODO: there is an error: This packet MUST be sent to the client and not to the service server
			// Signal that the node cannot be found because the domain part is missing in the node attribute
			signalErrorCondition( request, 
					new PacketError( PacketError.Condition.undefined_condition, PacketError.Type.cancel ), 
					DocumentHelper.createElement("server-response-malformed"));
			return ;
		}
		
		// Check if subscription request was successfully processed by the service server
		if( subscriptionState.compareTo("subscribed") == 0 )
			processSubscribedResult( request, childElement );
		
		/*
		 *  TODO: according to XEP-60 check the other possible subscriptionState:
		 *  	6.1.4. Approval Required
		 *  	6.1.5. Configuration Required
		 *  
		 *  else if( subscriptionState.compareTo("pending") == 0 )
		 *  	processPendingResult( request, childElement );
		 *  else if( subscriptionState.compareTo("unconfigured") == 0 )
		 *  	processUnconfiguredResult( request, childElement );
		 */
		
	}
	
	private void processSubscribedResult( IQ request, Element childElement )
	{
		Element subscriptionElement = childElement.element("subscription");
		
		String incomingRequestID = pendingOutSubscribe.get( request.getID());
		PendingSubscriptionDetails incomingRequest = pendingInSubscribe.get( incomingRequestID );
		
		IQ response = new IQ( IQ.Type.result );
		
		response.setID( incomingRequestID );
		response.setFrom( pubSubService.getName());
		response.setTo( incomingRequest.getFromJID());
		response.setChildElement( childElement.createCopy());
		
		pendingOutSubscribe.remove( request.getID());
		pendingInSubscribe.remove( incomingRequestID );
		
		Map<String, SubscriptionDetails> userSubscriptions;
		/* 
		 * If the user has at least one active subscription then its bare JID
		 * is present as key within subscriptions map and it is possible to retrieve
		 * its subscriptions' details 
		 */
		if( pubSubService.hasSubscriptions( incomingRequest.getFromJID().toBareJID()))
			userSubscriptions =
				pubSubService.getSubscriptions( incomingRequest.getFromJID().toBareJID());
		else
		{
			// Otherwise it is necessary to create a new entry related to the user in subscriptions
			userSubscriptions = new TreeMap<String, SubscriptionDetails>(); 
			pubSubService.addSubscription( incomingRequest.getFromJID().toBareJID(), userSubscriptions );
		}								
		
		SubscriptionDetails subscriptionDetails = new SubscriptionDetails();
		
		if( incomingRequest.getSubscribingResource() == null )
			subscriptionDetails.setSubscribedWithBareJID(true);
		else
			subscriptionDetails.addResource( incomingRequest.getSubscribingResource());
		
		String subIDAttribute = subscriptionElement.attributeValue("subid");
		if( subIDAttribute != null && subIDAttribute.length() > 0 )
			subscriptionDetails.setSubscriptionID( subIDAttribute );
		
		userSubscriptions.put( incomingRequest.getSubscribingNode(), subscriptionDetails );
		
		pubSubService.sendPacket( response );
	}

	@SuppressWarnings( "unchecked" )
	private void processResultDiscoItems( IQ request, Element childElement )
	{
		// Check if there is an outgoing request related to the current disco-items result
		if( !pendingDiscoItem.containsKey( request.getID()))
			return ;
		
		// Retrieve all items discovered
		List<Element> itemList = childElement.elements("item");
		/*
		 * If there are no items available in the server domain it is impossible 
		 * to know pubsub-subdomain; so an error has to sent to the specific client 
		*/
		if( itemList.isEmpty())
			signalNoItemsDiscovered( request );
		else
		{
			PendingDiscoInfoDetails details = new PendingDiscoInfoDetails( request.getID());
			
			for( Element item : itemList )
			{
				String currentJID = item.attributeValue("jid");
				if( currentJID != null && currentJID.length() > 0 )
					details.addItem( currentJID );								
			}
			/*
			 * If there are no valid JIDs related to the items discovered it is impossible 
			 * to know pubsub-subdomain; so an error has to sent to the specific client 
			*/
			if( details.isEmpty())
				signalNoItemsDiscovered( request );
			else
			{	
				IQ discoInfoRequest = new IQ( IQ.Type.get );
				
				discoInfoRequest.setFrom( pubSubService.getName());
				discoInfoRequest.setTo( details.getNextItem());
				discoInfoRequest.setChildElement("query", AbstractComponent.NAMESPACE_DISCO_INFO );
				
				pendingDiscoInfo.put( discoInfoRequest.getID(), details );
				
				pubSubService.sendPacket( discoInfoRequest );
			}
		}
	}
	
	private void signalNoItemsDiscovered( IQ request )
	{
		String incomingRequestID = pendingDiscoItem.get( request.getID());
		PendingSubscriptionDetails incomingRequest = pendingInSubscribe.get( incomingRequestID );
		
		pendingInSubscribe.remove( incomingRequestID );
		pendingDiscoItem.remove( request.getID());
		
		IQ response = new IQ( IQ.Type.error );
		
		response.setID( incomingRequestID );
		response.setFrom( pubSubService.getName());
		response.setTo( incomingRequest.getFromJID());
		PacketError errorElement = 
			new PacketError( PacketError.Condition.service_unavailable, PacketError.Type.cancel );
		response.setChildElement( errorElement.getElement());
				
		pubSubService.sendPacket( response );
	}
	
	@SuppressWarnings( "unchecked" )
	private void processResultDiscoInfo( IQ request, Element childElement )
	{
		if( !pendingDiscoInfo.containsKey( request.getID()))
			return ;

		PendingDiscoInfoDetails details = pendingDiscoInfo.get( request.getID());
		List<Element> featureList = childElement.elements("feature");
		
		if( featureList.isEmpty())
			processNextDiscoItem( request );
		else
		{
			// Verify that current item supports pubsub service
			boolean existsPubSubFeature = false;
			for( Element feature : featureList )
			{
				String var = feature.attributeValue("var");
				if( var != null && var.length() > 0 )
				{
					if( PubSubNamespace.BASIC.getXmlns().compareTo( var ) == 0 )
					{
						String serviceServer = request.getFrom().toString();
						String incomingRequestID = pendingDiscoItem.get( details.getRequestID());
						PendingSubscriptionDetails incomingRequest = pendingInSubscribe.get( incomingRequestID );
						
						String serviceDomain = StringUtils.parseServer( incomingRequest.getSubscribingNode());
						pubSubDomain.put( serviceDomain, serviceServer );
						
						IQ forwardedRequest = new IQ( IQ.Type.set );
						
						forwardedRequest.setFrom( pubSubService.getName());
						forwardedRequest.setTo( serviceServer );
						forwardedRequest.setChildElement("pubsub", PubSubNamespace.BASIC.getXmlns());
						
						Element subscribeElement = DocumentHelper.createElement("subscribe");
						subscribeElement.addAttribute("node", incomingRequest.getSubscribingNode());
						subscribeElement.addAttribute("jid", incomingRequest.getFromJID().toBareJID());
						
						PacketExtension subscriptionExtension = new PacketExtension( subscribeElement );
						forwardedRequest.addExtension( subscriptionExtension );
						
						pendingOutSubscribe.put( forwardedRequest.getID(), incomingRequestID );
						pendingDiscoItem.remove( details.getRequestID());
						pendingDiscoInfo.remove( request.getID());
						
						pubSubService.sendPacket( forwardedRequest );
						
						existsPubSubFeature = true;
						break;
					}
				}
			}
			if( !existsPubSubFeature )
				processNextDiscoItem( request );
		}
	}
	
	private void processNextDiscoItem( IQ request )
	{
		PendingDiscoInfoDetails details = pendingDiscoInfo.get( request.getID());
		
		// If there are no more items discovered within a specific server
		if( details.isEmpty())
		{
			String incomingRequestID = pendingDiscoItem.get( details.getRequestID());
			PendingSubscriptionDetails incomingRequest = pendingInSubscribe.get( incomingRequestID );
			
			IQ response = new IQ( IQ.Type.error );
			
			response.setID( incomingRequestID );
			response.setFrom( pubSubService.getName());
			response.setTo( incomingRequest.getFromJID());
			PacketError errorElement = 
				new PacketError( PacketError.Condition.service_unavailable, PacketError.Type.cancel );
			response.setChildElement( errorElement.getElement());
			
			pubSubService.sendPacket( response );
			
			pendingInSubscribe.remove( incomingRequestID );
			pendingDiscoItem.remove( details.getRequestID());
			pendingDiscoInfo.remove( request.getID());
		}
		else
		{
			// Make a new disco-info IQ
			IQ discoInfoRequest = new IQ( IQ.Type.get );
			
			discoInfoRequest.setFrom( pubSubService.getName());
			discoInfoRequest.setTo( details.getNextItem());
			discoInfoRequest.setChildElement("query", AbstractComponent.NAMESPACE_DISCO_INFO );
			
			pendingDiscoInfo.remove( request.getID());
			pendingDiscoInfo.put( discoInfoRequest.getID(), details );
			
			pubSubService.sendPacket( discoInfoRequest );
		}
	}
	
	/*
	 * Error in outgoing disco-item request, notified from service provider:
	 * 		- send error to client  
	 *  	- remove the pending incoming subscription request
	 *  	- remove the pending discovery item request 
	*/
	private void errorOutDiscoItemRequest( IQ request )
	{
		String incomingRequestID = pendingDiscoItem.get( request.getID());
		PendingSubscriptionDetails userRequest = pendingInSubscribe.get( incomingRequestID );
		
		IQ errorResponse = new IQ( IQ.Type.error );
		errorResponse.setID( incomingRequestID );
		errorResponse.setFrom( pubSubService.getName());
		errorResponse.setTo( userRequest.getFromJID());
		errorResponse.setChildElement( ((IQ) request).getChildElement().createCopy());
		
		pendingDiscoItem.remove( request.getID());
		pendingInSubscribe.remove( incomingRequestID );
		
		pubSubService.sendPacket( errorResponse );		
	}

	/*
	 * Error in outgoing subscription request, notified from service provider:
	 * 		- send error to client   
	 *  	- remove the pending incoming subscribe request 
	 *  	- remove the pending outgoing subscribe request
	*/
	private void errorOutSubscriptionRequest( IQ request )
	{
		String incomingRequestID = pendingOutSubscribe.get( request.getID());
		PendingSubscriptionDetails userRequest = pendingInSubscribe.get( incomingRequestID );
		
		IQ errorResponse = new IQ( IQ.Type.error );
		errorResponse.setID( incomingRequestID );
		errorResponse.setFrom( pubSubService.getName());
		errorResponse.setTo( userRequest.getFromJID());
		errorResponse.setChildElement( ((IQ) request).getChildElement().createCopy());
		
		pendingInSubscribe.remove( incomingRequestID );
		pendingOutSubscribe.remove( request.getID());
		
		pubSubService.sendPacket( errorResponse );		
	}

	private void signalErrorCondition( Packet request, PacketError packetError, Element specificErrorElement )
	{
		IQ response = IQ.createResultIQ((IQ) request );
		response.setType( IQ.Type.error );
		response.setError( packetError );
		
		if( specificErrorElement != null )
			response.getError().getElement().add( specificErrorElement );

		pubSubService.sendPacket( response );
	}

	
}
